﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using DomainDriver.CommonUtilities.Reflectors;
using DomainDriver.DomainModeling.Attributes;
using DomainDriver.DomainModeling.DataProviders;
using DomainDriver.DomainModeling.DomainObjects;
using DomainDriver.DomainModeling.Queries;

namespace DomainDriver.DomainModeling.StorageManagers.EntityFrameworkStorage
{
    public class EntityFrameworkDatabaseStorageManager<KEY, KEYED_DOMAIN_OBJECT, STORED_KEY, CONTEXT> : QueryableStorageManagerBase<KEY, KEYED_DOMAIN_OBJECT>
        where KEYED_DOMAIN_OBJECT : class, IKeyedDomainObject<KEY, KEYED_DOMAIN_OBJECT>, IEfEntity<STORED_KEY>
        where CONTEXT : DbContext
    {
        protected bool m_DomainObjectImplementsAggregateRootInterface = false;

        public EntityFrameworkDatabaseStorageManager(IDataProvider dataProvider)
            : base(dataProvider)
        {
            Type contextType = typeof(CONTEXT);
            if (!dataProvider.DataSourceType.IsSubclassOf(contextType) && !dataProvider.DataSourceType.Equals(contextType))
            {
                throw new ApplicationException();
            }

            Type domainObjectType = typeof(KEYED_DOMAIN_OBJECT);
            Type aggregateRootInterfaceType = typeof(IEfAggregate<KEYED_DOMAIN_OBJECT>);
            if (null != domainObjectType.GetInterface(aggregateRootInterfaceType.Name))
            { m_DomainObjectImplementsAggregateRootInterface = true; }
        }

        protected CONTEXT DataContext
        {
            get { return DataProvider.DataSource as CONTEXT; }
        }

        protected override IEnumerable<KEYED_DOMAIN_OBJECT> GetQueryableDataStore()
        {
            DbSet<KEYED_DOMAIN_OBJECT> domainObjectSet = DataContext.Set<KEYED_DOMAIN_OBJECT>();
            return domainObjectSet;
        }

        protected override Func<KEYED_DOMAIN_OBJECT, bool> GetKeyMatchingPredicate(IEnumerable<KEY> keys)
        {
            List<STORED_KEY> convertedKeys = new List<STORED_KEY>();
            foreach (KEY key in keys)
            {
                STORED_KEY convertedKey = (STORED_KEY)Convert.ChangeType(key, typeof(STORED_KEY));
                convertedKeys.Add(convertedKey);
            }

            Func<KEYED_DOMAIN_OBJECT, bool> keyMatchingPredicate = ((KEYED_DOMAIN_OBJECT domainObject) => convertedKeys.Contains(domainObject.EF_Id));
            return keyMatchingPredicate;
        }

        protected override ICollection<KEY> ReadKeys(Func<KEYED_DOMAIN_OBJECT, bool> predicate)
        {
            IEnumerable<KEYED_DOMAIN_OBJECT> domainObjects = PerformQuery(predicate, false);
            List<KEY> matchingKeys = new List<KEY>();

            foreach (KEYED_DOMAIN_OBJECT domainObject in domainObjects)
            {
                matchingKeys.Add(domainObject.Key);
            }
            return matchingKeys;
        }

        protected override ICollection<DESC> ReadDescriptors<DESC>(Func<KEYED_DOMAIN_OBJECT, bool> predicate)
        {
            IEnumerable<KEYED_DOMAIN_OBJECT> domainObjects = PerformQuery(predicate, false);
            List<DESC> matchingDescriptors = new List<DESC>();

            foreach (KEYED_DOMAIN_OBJECT domainObject in domainObjects)
            {
                DESC descriptor = new DESC();
                descriptor.DescribedObject = domainObject;
                matchingDescriptors.Add(descriptor);
            }
            return matchingDescriptors;
        }

        protected override ICollection<KEYED_DOMAIN_OBJECT> ReadStoredObjects(Func<KEYED_DOMAIN_OBJECT, bool> predicate)
        {
            IEnumerable<KEYED_DOMAIN_OBJECT> domainObjects = PerformQuery(predicate, true);
            List<KEYED_DOMAIN_OBJECT> matchingObjects = new List<KEYED_DOMAIN_OBJECT>();

            foreach (KEYED_DOMAIN_OBJECT domainObject in domainObjects)
            {
                matchingObjects.Add(domainObject);
            }
            return matchingObjects;
        }

        protected IEnumerable<KEYED_DOMAIN_OBJECT> PerformQuery(Func<KEYED_DOMAIN_OBJECT, bool> queryPredicate, bool readAggregates)
        {
            DbSet<KEYED_DOMAIN_OBJECT> domainObjectSet = DataContext.Set<KEYED_DOMAIN_OBJECT>();
            IEnumerable<KEYED_DOMAIN_OBJECT> queryResults = domainObjectSet.Where<KEYED_DOMAIN_OBJECT>(queryPredicate);

            if (!m_DomainObjectImplementsAggregateRootInterface || !readAggregates)
            { return queryResults; }

            Dictionary<string, object> batchReadState = new Dictionary<string, object>();
            Type rootType = typeof(KEYED_DOMAIN_OBJECT);
            batchReadState.Add(rootType.Name, queryResults);

            foreach (IEfAggregate<KEYED_DOMAIN_OBJECT> domainObject in queryResults)
            {
                domainObject.ReadNestedAggregateValues(DataContext, queryPredicate, batchReadState);
            }
            return queryResults;
        }

        protected override void WriteObject(KEYED_DOMAIN_OBJECT domainObject, ChangeActionType changeAction)
        {
            DbSet<KEYED_DOMAIN_OBJECT> domainObjectSet = DataContext.Set<KEYED_DOMAIN_OBJECT>();

            if (changeAction == ChangeActionType.Create)
            {
                if (m_DomainObjectImplementsAggregateRootInterface)
                {
                    (domainObject as IEfAggregate<KEYED_DOMAIN_OBJECT>).AddNestedAggregateValues(DataContext);
                }
                domainObjectSet.Add(domainObject);
                DataContext.SaveChanges();
                return;
            }

            List<KEY> domainObjectKeys = new List<KEY>();
            domainObjectKeys.Add(domainObject.Key);
            Func<KEYED_DOMAIN_OBJECT, bool> queryPredicate = GetKeyMatchingPredicate(domainObjectKeys);
            IEnumerable<KEYED_DOMAIN_OBJECT> queryResults = PerformQuery(queryPredicate, true);

            KEYED_DOMAIN_OBJECT original = queryResults.First<KEYED_DOMAIN_OBJECT>();
            KEYED_DOMAIN_OBJECT copyOfOriginal = original.Copy();

            if (changeAction == ChangeActionType.Delete)
            {
                domainObjectSet.Remove(original);
                // NOTE: Need to delete nested objects before saving changes
                if (m_DomainObjectImplementsAggregateRootInterface)
                {
                    (copyOfOriginal as IEfAggregate<KEYED_DOMAIN_OBJECT>).DeleteNestedAggregateValues(DataContext);
                }
                DataContext.SaveChanges();
                return;
            }

            // NOTE: Need to copy changes to domain object read directly from DbSet for change tracker to save changes
            KEYED_DOMAIN_OBJECT updatedOriginal = original;
            domainObject.CopyTo(updatedOriginal);
            if (m_DomainObjectImplementsAggregateRootInterface)
            {
                (updatedOriginal as IEfAggregate<KEYED_DOMAIN_OBJECT>).UpdateNestedAggregateValues(DataContext, copyOfOriginal);
            }
            DataContext.SaveChanges();
        }
    }
}